Skip to content

fix(types): accept an explicit undefined wherever a consumer writes an optional member - #418

Open
YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:rncss/types-optional-undefined
Open

YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:rncss/types-optional-undefined

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

Problem

React Native declares its own optional props as ?: T | undefined, while the props this package adds are bare ?: string. Under exactOptionalPropertyTypes the two differ, so this fails to compile:

<View className={condition ? "p-4" : undefined} />
error TS2769: No overload matches this call.
  Overload 1 of 2, '(props: ViewProps): View', gave the following error.
    Type '{ className: string | undefined; }' is not assignable to type 'Readonly<ViewProps>' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.

styled() has the same bare members: the className its unmapped overload adds, passThrough, and a mapping's nativeStyleMapping and its entries; so do the options a consumer writes for the compiler, Metro, the jest helper and useNativeCss.

A nativeStyleMapping entry left undefined is also a runtime error: mappingToConfig keeps it, and the native style mapping calls path.split(".") on it:

const StyledView = styled(View, {
  className: {
    target: "style",
    nativeStyleMapping: { color: "accessibilityLabel", backgroundColor: undefined },
  },
});

// .subject { color: blue; background-color: red; }
// Rendering throws "Cannot read properties of undefined (reading 'split')",
// and the render React recovers with has no backgroundColor.
<StyledView className="subject" />;

Solution

  • Every optional member a consumer writes takes | undefined: the props types.d.ts adds; the props styled() adds, passThrough, nativeStyleMapping and its entries in runtime.types.ts; CompilerOptions, Metro's options, the jest helper's debug and useNativeCss's Config; and the Effect that get() takes on the observables StyleCollection exposes. Without the flag the types are unchanged.
  • CompilerOptions.logger is ((message: string) => void) | Debugger | undefined, where (message: string) => void | Debugger made Debugger the function's return type.
  • mappingToConfig drops a nativeStyleMapping entry left undefined.

Tests

  • src/__tests__/native/styled.test.ios.tsx: a mapping entry set to undefined maps nothing, and the entry beside it still maps. On main its render throws, and yarn typecheck rejects its undefined entry.
  • src/__tests__/types/optional-members.test.ts builds a program with exactOptionalPropertyTypes on and walks what a consumer writes: the props types.d.ts adds, runtime.types.ts, and what each entry point of package.json's exports takes. It fails on any optional member that rejects undefined, so a member added later is held to it too; on main it names the 42 places this PR widens.

Verification

yarn lint · yarn typecheck · yarn test 1418 passed · yarn build and node --test node/__tests__/*.mjs · yarn example expo export --platform web · commitlint: all green on this head on Ubuntu, with Node 26.12.1 (.nvmrc pins 22.18.0), and the iOS dev build on macOS 27.2 with Xcode 27.2, where ci.yml pins 26.6.

The className example above and the mapping snippet, each on main and on this branch, against the sources and against the built package, are in this comment.

In an Expo app, expo/types declares className on ViewProps, TextProps and other React Native props without | undefined, and expo-env.d.ts loads it before this package's declarations, so there the className example keeps failing until Expo's declarations take | undefined too, which expo/expo#43508 tracks.

Merge order

@YevheniiKotyrlo

YevheniiKotyrlo commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor Author

className={maybe} under the flag — main against this PR

import { Text, View } from "react-native";
import { styled } from "react-native-css";

declare const condition: boolean;
const StyledView = styled(View);

export const elements = [
  <View className={condition ? "p-4" : undefined} />,
  <Text className={condition ? "p-4" : undefined} />,
  <StyledView className={condition ? "p-4" : undefined} />,
];

tsc with strict and exactOptionalPropertyTypes, over this file and the package's types.d.ts:

errors in the file
main (df32ce30) three TS2769: No overload matches this call, one per element
this PR none

Importing the package built with yarn build, with /// <reference types="react-native-css/types" />, the same elements, the mapping snippet from the body, and the options of styled(), compile, registerCSS and withReactNativeCSS each written undefined give 8 errors on main and none on this PR; with skipLibCheck off, the package's built declarations report nothing on either.

A mapping entry left undefined — main against this PR

The snippet from the body, rendered through the repo's jest with a window.dispatchEvent stub, so React's report of the error it recovered from reaches console.error, against the sources and against the built dist/commonjs alike:

errors accessibilityLabel style
main (df32ce30) There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root., caused by Cannot read properties of undefined (reading 'split') #00f {}
this PR none #00f { backgroundColor: "#f00" }

Windows 11 Pro 10.0.26300, Node 26.12.1, TypeScript 7.0.2 and 6.0.3, which give the same results.

@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the rncss/types-optional-undefined branch from 6c6da94 to d10ad90 Compare October 11, 2026 12:35
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as draft October 20, 2026 09:53
@YevheniiKotyrlo YevheniiKotyrlo changed the title fix(types): accept an explicit undefined on every prop this package adds fix(types): accept an explicit undefined on every prop and styled() option this package adds Oct 20, 2026
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the rncss/types-optional-undefined branch from 8c8919f to 49ff1c9 Compare October 20, 2026 15:02
@YevheniiKotyrlo YevheniiKotyrlo changed the title fix(types): accept an explicit undefined on every prop and styled() option this package adds fix(types): accept an explicit undefined on every prop, styled() option and mapping entry this package adds Oct 20, 2026
@YevheniiKotyrlo YevheniiKotyrlo changed the title fix(types): accept an explicit undefined on every prop, styled() option and mapping entry this package adds fix(types): accept an explicit undefined on every added prop, styled() option and mapping entry Oct 20, 2026
@YevheniiKotyrlo YevheniiKotyrlo changed the title fix(types): accept an explicit undefined on every added prop, styled() option and mapping entry fix(types): accept an explicit undefined wherever a consumer writes an optional member Oct 20, 2026
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the rncss/types-optional-undefined branch from e283081 to 157684a Compare October 22, 2026 11:52
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 22, 2026 12:06

@sammoore sammoore left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first glance, would love to merge the JSX types error caused by exactOptionalPropertyTypes, but there's a bunch of seemingly unrelated / heavy-handed changes in here:

  • NativeStyleMapping validation (we can just filter undefined values to avoid the resulting runtime error for now), the new isPath (also changes runtime semantics for something seemingly unrelated), etc
  • the parentheses around the logger fn vs. Debugger (great catch! would love to merge this)
  • skills/ updates

Generally speaking, the minimal fix here for the issue as outlined in the description seems to just be updating the types to include | undefined and then filtering the undefined.

Let me know what your thoughts @YevheniiKotyrlo

@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

@sammoore yes, you are right, this PR is a bit poor taste from my side with some indeed unrelated changes in the docs and etc. I believe this changes slipped in from my patch regeneration tool that I had to write to constantly regenerate patches from my PRs on top of current version I was using. I will code review it and see how I can improve it. Thanks!

@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as draft November 5, 2026 11:22
…n optional member

Under exactOptionalPropertyTypes a bare `?: T` rejects the explicit
undefined React Native's own optional props accept, so
`className={condition ? "p-4" : undefined}` did not compile. Every
optional member a consumer writes now takes `| undefined`: the props
types.d.ts adds, styled()'s props, options and mapping, the options of
the compiler, Metro, the jest helper and useNativeCss, and the Effect an
observable's get() takes. CompilerOptions.logger's function type is
parenthesised, so Debugger is no longer read as its return type.

A mapping entry left undefined now type-checks, and mappingToConfig
drops it rather than passing it on to the native style mapping, which
called `.split(".")` on it at render.

optional-members.test.ts walks, with the type checker and the flag on,
what a consumer writes to each entry point package.json exports, and
fails on any optional member that rejects undefined.
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the rncss/types-optional-undefined branch from 157684a to 6d12eb2 Compare November 5, 2026 13:58
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

@sammoore the branch is now one commit on current main, 6d12eb2, with that minimal fix:

  • every optional member a consumer writes takes | undefined, and mappingToConfig drops a nativeStyleMapping entry left undefined;
  • the NativeStyleMapping validation, isPath, the rewrite of the mapping's type, and the skills/ and DEVELOPMENT.md edits are gone;
  • the logger parentheses stay: | undefined goes on that member, and without them it would join Debugger in the function's return type;
  • the tests are the dropped-entry case in styled.test.ios.tsx, and optional-members.test.ts, which walks the exported types with the flag on and fails on any optional member a consumer writes that rejects undefined.

@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review November 5, 2026 14:18

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants